/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.cdi.transaction;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;

import javax.transaction.TransactionRolledbackException;

import org.apache.camel.AsyncCallback;
import org.apache.camel.AsyncProcessor;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.LoggingLevel;
import org.apache.camel.Navigate;
import org.apache.camel.Processor;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.processor.ErrorHandlerSupport;
import org.apache.camel.processor.exceptionpolicy.ExceptionPolicyStrategy;
import org.apache.camel.spi.ShutdownPrepared;
import org.apache.camel.util.ExchangeHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ServiceHelper;

/**
 * Does transactional execution according given policy. This class is based on
 * {@link org.apache.camel.spring.spi.TransactionErrorHandler} excluding
 * redelivery functionality. In the Spring implementation redelivering is done
 * within the transaction which is not appropriate in JTA since every error
 * breaks the current transaction.
 */
public class TransactionErrorHandler extends ErrorHandlerSupport
        implements AsyncProcessor, ShutdownPrepared, Navigate<Processor> {

    protected final Processor output;

    protected volatile boolean preparingShutdown;

    private ExceptionPolicyStrategy exceptionPolicy;

    private JtaTransactionPolicy transactionPolicy;

    private final String transactionKey;

    private final LoggingLevel rollbackLoggingLevel;

    /**
     * Creates the transaction error handler.
     *
     * @param camelContext
     *            the camel context
     * @param output
     *            outer processor that should use this default error handler
     * @param exceptionPolicyStrategy
     *            strategy for onException handling
     * @param transactionPolicy
     *            the transaction policy
     * @param executorService
     *            the {@link java.util.concurrent.ScheduledExecutorService} to
     *            be used for redelivery thread pool. Can be <tt>null</tt>.
     * @param rollbackLoggingLevel
     *            logging level to use for logging transaction rollback occurred
     */
    public TransactionErrorHandler(CamelContext camelContext, Processor output,
            ExceptionPolicyStrategy exceptionPolicyStrategy, JtaTransactionPolicy transactionPolicy,
            ScheduledExecutorService executorService, LoggingLevel rollbackLoggingLevel) {
        this.output = output;
        this.transactionPolicy = transactionPolicy;
        this.rollbackLoggingLevel = rollbackLoggingLevel;
        this.transactionKey = ObjectHelper.getIdentityHashCode(transactionPolicy);

        setExceptionPolicy(exceptionPolicyStrategy);
    }

    public void process(Exchange exchange) throws Exception {
        // we have to run this synchronously as a JTA Transaction does *not*
        // support using multiple threads to span a transaction
        if (exchange.getUnitOfWork().isTransactedBy(transactionKey)) {
            // already transacted by this transaction template
            // so lets just let the error handler process it
            processByErrorHandler(exchange);
        } else {
            // not yet wrapped in transaction so lets do that
            // and then have it invoke the error handler from within that
            // transaction
            processInTransaction(exchange);
        }
    }

    public boolean process(Exchange exchange, AsyncCallback callback) {
        // invoke this synchronous method as JTA Transaction does *not*
        // support using multiple threads to span a transaction
        try {
            process(exchange);
        } catch (Throwable e) {
            exchange.setException(e);
        }

        // notify callback we are done synchronously
        callback.done(true);
        return true;
    }

    protected void processInTransaction(final Exchange exchange) throws Exception {
        // is the exchange redelivered, for example JMS brokers support such
        // details
        Boolean externalRedelivered = exchange.isExternalRedelivered();
        final String redelivered = externalRedelivered != null ? externalRedelivered.toString() : "unknown";
        final String ids = ExchangeHelper.logIds(exchange);

        try {
            // mark the beginning of this transaction boundary
            exchange.getUnitOfWork().beginTransactedBy(transactionKey);
            // do in transaction
            logTransactionBegin(redelivered, ids);
            doInTransactionTemplate(exchange);
            logTransactionCommit(redelivered, ids);
        } catch (TransactionRolledbackException e) {
            // do not set as exception, as its just a dummy exception to force
            // spring TX to rollback
            logTransactionRollback(redelivered, ids, null, true);
        } catch (Throwable e) {
            exchange.setException(e);
            logTransactionRollback(redelivered, ids, e, false);
        } finally {
            // mark the end of this transaction boundary
            exchange.getUnitOfWork().endTransactedBy(transactionKey);
        }

        // if it was a local rollback only then remove its marker so outer
        // transaction wont see the marker
        Boolean onlyLast = (Boolean) exchange.removeProperty(Exchange.ROLLBACK_ONLY_LAST);
        if (onlyLast != null && onlyLast) {
            // we only want this logged at debug level
            if (log.isDebugEnabled()) {
                // log exception if there was a cause exception so we have the
                // stack trace
                Exception cause = exchange.getException();
                if (cause != null) {
                    log.debug("Transaction rollback ({}) redelivered({}) for {} "
                        + "due exchange was marked for rollbackOnlyLast and caught: ",
                        transactionKey, redelivered, ids, cause);
                } else {
                    log.debug("Transaction rollback ({}) redelivered({}) for {} "
                        + "due exchange was marked for rollbackOnlyLast",
                        transactionKey, redelivered, ids);
                }
            }
            // remove caused exception due we was marked as rollback only last
            // so by removing the exception, any outer transaction will not be
            // affected
            exchange.setException(null);
        }
    }

    public void setTransactionPolicy(JtaTransactionPolicy transactionPolicy) {
        this.transactionPolicy = transactionPolicy;
    }

    protected void doInTransactionTemplate(final Exchange exchange) throws Throwable {
        // spring transaction template is working best with rollback if you
        // throw it a runtime exception
        // otherwise it may not rollback messages send to JMS queues etc.
        transactionPolicy.run(new JtaTransactionPolicy.Runnable() {

            @Override
            public void run() throws Throwable {
                // wrapper exception to throw if the exchange failed
                // IMPORTANT: Must be a runtime exception to let Spring regard
                // it as to do "rollback"
                Throwable rce;

                // and now let process the exchange by the error handler
                processByErrorHandler(exchange);

                // after handling and still an exception or marked as rollback
                // only then rollback
                if (exchange.getException() != null || exchange.isRollbackOnly()) {

                    // wrap exception in transacted exception
                    if (exchange.getException() != null) {
                        rce = exchange.getException();
                    } else {
                        // create dummy exception to force spring transaction
                        // manager to rollback
                        rce = new TransactionRolledbackException();
                    }

                    // throw runtime exception to force rollback (which works
                    // best to rollback with Spring transaction manager)
                    if (log.isTraceEnabled()) {
                        log.trace("Throwing runtime exception to force transaction to rollback on {}",
                                transactionPolicy);
                    }
                    throw rce;
                }
            }
        });
    }

    /**
     * Processes the {@link Exchange} using the error handler.
     * <p/>
     * This implementation will invoke ensure this occurs synchronously, that
     * means if the async routing engine did kick in, then this implementation
     * will wait for the task to complete before it continues.
     *
     * @param exchange
     *            the exchange
     */
    protected void processByErrorHandler(final Exchange exchange) {
        try {
            output.process(exchange);
        } catch (Throwable e) {
            throw new RuntimeCamelException(e);
        }
    }

    /**
     * Logs the transaction begin
     */
    private void logTransactionBegin(String redelivered, String ids) {
        if (log.isDebugEnabled()) {
            log.debug("Transaction begin ({}) redelivered({}) for {})",
                    transactionKey, redelivered, ids);
        }
    }

    /**
     * Logs the transaction commit
     */
    private void logTransactionCommit(String redelivered, String ids) {
        if ("true".equals(redelivered)) {
            // okay its a redelivered message so log at INFO level if
            // rollbackLoggingLevel is INFO or higher
            // this allows people to know that the redelivered message was
            // committed this time
            if (rollbackLoggingLevel == LoggingLevel.INFO || rollbackLoggingLevel == LoggingLevel.WARN
                    || rollbackLoggingLevel == LoggingLevel.ERROR) {
                log.info("Transaction commit ({}) redelivered({}) for {})",
                        transactionKey, redelivered, ids);
                // return after we have logged
                return;
            }
        }

        // log non redelivered by default at DEBUG level
        log.debug("Transaction commit ({}) redelivered({}) for {})", transactionKey, redelivered, ids);
    }

    /**
     * Logs the transaction rollback.
     */
    private void logTransactionRollback(String redelivered, String ids, Throwable e, boolean rollbackOnly) {
        if (rollbackLoggingLevel == LoggingLevel.OFF) {
            return;
        } else if (rollbackLoggingLevel == LoggingLevel.ERROR && log.isErrorEnabled()) {
            if (rollbackOnly) {
                log.error("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
                        transactionKey, redelivered, ids);
            } else {
                log.error("Transaction rollback ({}) redelivered({}) for {} caught: {}",
                        transactionKey, redelivered, ids, e.getMessage());
            }
        } else if (rollbackLoggingLevel == LoggingLevel.WARN && log.isWarnEnabled()) {
            if (rollbackOnly) {
                log.warn("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
                        transactionKey, redelivered, ids);
            } else {
                log.warn("Transaction rollback ({}) redelivered({}) for {} caught: {}",
                        transactionKey, redelivered, ids, e.getMessage());
            }
        } else if (rollbackLoggingLevel == LoggingLevel.INFO && log.isInfoEnabled()) {
            if (rollbackOnly) {
                log.info("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
                        transactionKey, redelivered, ids);
            } else {
                log.info("Transaction rollback ({}) redelivered({}) for {} caught: {}",
                        transactionKey, redelivered, ids, e.getMessage());
            }
        } else if (rollbackLoggingLevel == LoggingLevel.DEBUG && log.isDebugEnabled()) {
            if (rollbackOnly) {
                log.debug("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
                        transactionKey, redelivered, ids);
            } else {
                log.debug("Transaction rollback ({}) redelivered({}) for {} caught: {}",
                        transactionKey, redelivered, ids, e.getMessage());
            }
        } else if (rollbackLoggingLevel == LoggingLevel.TRACE && log.isTraceEnabled()) {
            if (rollbackOnly) {
                log.trace("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
                        transactionKey, redelivered, ids);
            } else {
                log.trace("Transaction rollback ({}) redelivered({}) for {} caught: {}",
                        transactionKey, redelivered, ids, e.getMessage());
            }
        }
    }

    public void setExceptionPolicy(ExceptionPolicyStrategy exceptionPolicy) {
        this.exceptionPolicy = exceptionPolicy;
    }

    public ExceptionPolicyStrategy getExceptionPolicy() {
        return exceptionPolicy;
    }

    @Override
    public Processor getOutput() {
        return output;
    }

    @Override
    protected void doStart() throws Exception {
        ServiceHelper.startServices(output);
        preparingShutdown = false;
    }

    @Override
    protected void doStop() throws Exception {
        // noop, do not stop any services which we only do when shutting down
        // as the error handler can be context scoped, and should not stop in
        // case a route stops
    }

    @Override
    protected void doShutdown() throws Exception {
        ServiceHelper.stopAndShutdownServices(output);
    }

    @Override
    public boolean supportTransacted() {
        return true;
    }

    public boolean hasNext() {
        return output != null;
    }

    @Override
    public List<Processor> next() {
        if (!hasNext()) {
            return null;
        }
        List<Processor> answer = new ArrayList<>(1);
        answer.add(output);
        return answer;
    }

    @Override
    public void prepareShutdown(boolean suspendOnly, boolean forced) {
        // prepare for shutdown, eg do not allow redelivery if configured
        log.trace("Prepare shutdown on error handler {}", this);
        preparingShutdown = true;
    }
}
